import {
  CreateTableMember,
  CreateTableBuilder,
  Statement,
  ColumnDeclareForAdd,
  ColumnDeclareForAlter,
  KeyColumn,
  XObjectName,
  DbType,
  SQL,
  ExecutableStatement,
} from '../core';

import { MigrateBuilder } from './migrate-builder';
import {
  CheckConstraintSchema,
  ColumnSchema,
  ConstraintSchema,
  DatabaseSchema,
  ForeignKeySchema,
  IndexSchema,
  PrimaryKeySchema,
  SequenceSchema,
  TableSchema,
  // UniqueConstraintSchema,
} from '../orm';
import { pack, sortByDependency } from './util';
import { ObjectDifference } from './compare';
import { SchemaComparator } from './schema-comparator';

export abstract class MigrateScripter<T extends string | ExecutableStatement> {
  constructor(private comparator: SchemaComparator) {}
  protected beforeCodes: T[] = [];
  protected afterCodes: T[] = [];
  protected middleCodes: T[] = [];
  protected seedDataCodes: Map<TableSchema, T> = new Map();

  clear() {
    this.afterCodes = [];
    this.beforeCodes = [];
    this.seedDataCodes.clear();
    this.middleCodes = [];
  }

  /**
   * 生成迁移代码
   */
  migrate(
    defaultSchema: string,
    source: DatabaseSchema | undefined,
    target: DatabaseSchema | undefined
  ): void {
    if (!target && source) {
      // 创建数据库,由于事务内不允许创建数据库注释起来
      // this.createDatabase(differences.source);
      // this.useDatabase(differences.source.name);
      source.tables.forEach(table => this.createTableAndMembers(table));
      source.sequences.forEach(sequence => this.createSequence(sequence));
    } else if (target && !source) {
      // 删除数据库
      // this.dropDatabase(differences.this.target.name);
      target.tables.forEach(table => this.dropTable(table));
      target.sequences.forEach(sequence => this.dropSequence(sequence));
    } else {
      const differences = this.comparator.compareSchema(
        defaultSchema,
        source,
        target
      );
      if (!differences) return;
      if (differences.changes?.tables) {
        differences.changes.tables.removeds.forEach(dropTable =>
          this.dropTableAndMembers(dropTable)
        );
        differences.changes.tables.addeds.forEach(addedTable =>
          this.createTableAndMembers(addedTable)
        );
        differences.changes.tables.changes.forEach(tableDifference =>
          this.alterTableAndMembers(tableDifference)
        );
      }

      if (differences.changes?.sequences) {
        for (const sequence of differences.changes.sequences.addeds || []) {
          this.createSequence(sequence);
        }
        for (const sequence of differences.changes.sequences.removeds || []) {
          this.dropSequence(sequence);
        }
      }
    }
  }

  getScripts() {
    const results = [
      ...this.beforeCodes,
      ...this.middleCodes,
      ...this.afterCodes,
    ];
    if (this.seedDataCodes.size > 0) {
      const tables = Array.from(this.seedDataCodes.keys());
      const sortedTables = sortByDependency(tables, t =>
        t.foreignKeys
          .map(
            fk =>
              tables.find(
                table =>
                  table.name === fk.referenceTable &&
                  table.schema === fk.referenceSchema
              )!
          )
          .filter(p => p)
      );
      results.push(...sortedTables.map(p => this.seedDataCodes.get(p)!));
    }
    return results;
  }

  abstract insertSeedData(table: TableSchema, data: any[]): void;

  // abstract useDatabase(name: string): void;

  // abstract createDatabase(database: DatabaseSchema): void;

  // abstract alterDatabase(database: DatabaseSchema): void;

  // abstract dropDatabase(name: string): void;

  abstract createIndex(table: XObjectName, index: IndexSchema): void;

  abstract dropIndex(table: XObjectName, name: string): void;

  abstract addForeignKey(table: XObjectName, fk: ForeignKeySchema): void;

  abstract dropForeignKey(table: XObjectName, name: string): void;

  abstract commentColumn(
    table: XObjectName<string>,
    name: string,
    comment?: string
  ): void;

  abstract commentTable(table: XObjectName, comment?: string): void;

  abstract commentIndex(
    table: XObjectName,
    name: string,
    comment?: string
  ): void;

  abstract commentConstraint(
    table: XObjectName,
    name: string,
    comment?: string
  ): void;

  abstract commentForeignKey(
    table: XObjectName,
    name: string,
    comment?: string
  ): void;

  abstract setDefaultValue(
    table: XObjectName,
    column: string,
    defaultValue: string
  ): void;

  abstract dropDefaultValue(table: XObjectName, column: string): void;

  abstract setAutoRowflag(table: XObjectName, column: string): void;

  abstract dropAutoRowflag(table: XObjectName, column: string): void;

  abstract dropIdentity(table: XObjectName, column: string): void;

  abstract setIdentity(
    table: XObjectName,
    column: string,
    start: number,
    increment: number
  ): void;

  abstract createTable(table: TableSchema): void;

  createTableAndMembers(table: TableSchema): void {
    this.createTable(table);

    if (table.primaryKey?.comment) {
      this.commentConstraint(
        table,
        table.primaryKey.name,
        table.primaryKey.comment
      );
    }

    table.columns.forEach(column => {
      if (column.isRowflag) {
        this.setAutoRowflag(table, column.name);
      }
      if (column.comment) {
        this.commentColumn(table, column.name, column.comment);
      }
    });

    if (table.comment) {
      this.commentTable(table, table.comment);
    }

    table.indexes.forEach(index => {
      this.createIndex(table, index);
      if (index.comment) {
        this.commentIndex(table, index.name, index.comment);
      }
    });

    table.foreignKeys.forEach(fk => {
      this.addForeignKey(table, fk);
      if (fk.comment) {
        this.commentForeignKey(table, fk.name, fk.comment);
      }
    });

    table.constraints.forEach(constraint => {
      this.addConstraint(table, constraint);
      if (constraint.comment) {
        this.commentConstraint(table, constraint.name, constraint.comment);
      }
    });

    if (table.seedData?.length) {
      this.insertSeedData(table, table.seedData);
    }
  }

  alterTableAndMembers(tableChanges: ObjectDifference<TableSchema>): void {
    const table = tableChanges.target!;
    // DROP PRIMARY KEY
    if (tableChanges.changes?.primaryKey) {
      if (tableChanges.changes.primaryKey.removed) {
        this.dropPrimaryKey(
          table,
          tableChanges.changes.primaryKey.removed.name
        );
      }

      if (tableChanges.changes?.primaryKey.changes) {
        const pk = tableChanges.changes?.primaryKey;
        if (pk.changes?.columns || pk.changes?.isNonclustered) {
          this.dropPrimaryKey(
            table,
            tableChanges.changes.primaryKey.target!.name
          );
        }
      }
    }

    // DROP CONSTRAINTS
    if (tableChanges.changes?.constraints) {
      for (const constraint of tableChanges.changes.constraints.removeds ||
        []) {
        this.dropConstraint(table, constraint);
      }

      for (const { source, target, changes } of tableChanges.changes.constraints
        .changes || []) {
        if (
          changes?.kind !== target?.kind ||
          (source?.kind === 'CHECK' &&
            (
              tableChanges.changes
                .constraints as ObjectDifference<CheckConstraintSchema>
            ).changes?.sql)
          // || (source?.kind === 'UNIQUE' &&
          // (
          //   tableChanges.changes
          //     .constraints as ObjectDifference<UniqueConstraintSchema>
          // ).changes?.columns)
        ) {
          this.dropConstraint(table, target!);
        }
      }
    }
    // DROP INDEXES
    if (tableChanges.changes?.indexes) {
      for (const index of tableChanges.changes.indexes.removeds || []) {
        this.dropIndex(table, index.name);
      }

      for (const { source, target, changes } of tableChanges.changes.indexes
        .changes || []) {
        if (changes?.columns || changes?.isClustered || changes?.isUnique) {
          this.dropIndex(table, target!.name);
        }
      }
    }

    // CHANGE COLUMNS
    if (tableChanges.changes?.columns) {
      for (const col of tableChanges.changes.columns.removeds || []) {
        // const fk = findTargetForeignKey(({ table, foreignKey }) => isNameEquals(tableName, name))
        this.dropColumn(table, col.name);
      }
      for (const column of tableChanges.changes.columns.addeds || []) {
        this.addColumn(table, column);
        if (column.comment) {
          this.commentColumn(table, column.name, column.comment);
        }
      }

      for (const { target, source, changes } of tableChanges.changes.columns
        .changes || []) {
        if (!changes) continue;
        // 如果类型或者是否可空变化
        if (
          source!.type.replace(/ /g, '').toUpperCase() !==
            target!.type.replace(/ /g, '').toUpperCase() ||
          changes.isNullable
        ) {
          this.alterColumn(table, source!);
        }

        if (changes.defaultValue) {
          if (!changes.defaultValue.source) {
            this.dropDefaultValue(table, target!.name);
          } else {
            this.setDefaultValue(
              table,
              source!.name,
              changes.defaultValue.source
            );
          }
        }

        if (changes.isRowflag) {
          if (source!.isRowflag) {
            this.setAutoRowflag(table, source!.name);
          } else {
            this.dropAutoRowflag(table, source!.name);
          }
        }

        if (
          changes.isIdentity ||
          changes.identityIncrement ||
          changes.identityIncrement
        ) {
          if (!source!.isIdentity) {
            this.annotation(
              '// 敬告：因为需要重建表，在mssql中尚未实现该功能。'
            );
            this.dropIdentity(table, target!.name);
          } else {
            this.annotation(
              '// 敬告：因为需要重建表，在mssql中尚未实现该功能。'
            );
            this.setIdentity(
              table,
              source!.name,
              source!.identityStartValue!,
              source!.identityIncrement!
            );
          }
        }

        if (changes.comment) {
          this.commentColumn(table, source!.name, source?.comment);
        }
      }
    }

    // ADD PRIMARY KEY AND CHANGE COMMENT
    if (tableChanges.changes?.primaryKey) {
      // 新增主键
      if (tableChanges.changes.primaryKey.added) {
        const pk = tableChanges.changes.primaryKey.added;
        this.addPrimaryKey(table, pk);
        if (pk.comment) {
          this.commentConstraint(table, pk.name, pk.comment);
        }
      }

      // 修改主健重新添加
      if (tableChanges.changes?.primaryKey?.changes) {
        const pk = tableChanges.changes?.primaryKey;
        if (pk.changes?.columns || pk.changes?.isNonclustered) {
          this.addPrimaryKey(table, pk.source!);
          if (pk.source?.comment) {
            this.commentConstraint(table, pk.source.name, pk.source.comment);
          }
        } else if (pk.changes?.comment) {
          this.commentConstraint(table, pk.source!.name, pk.source?.comment);
        }
      }
    }

    // FOREIGN KEY 因为有删除提前和添加置后，无须注意顺序
    if (tableChanges.changes?.foreignKeys) {
      for (const fk of tableChanges.changes?.foreignKeys?.addeds || []) {
        this.addForeignKey(table, fk);
        if (fk.comment) {
          this.commentConstraint(table, fk.name, fk.comment);
        }
      }

      for (const { name } of tableChanges.changes?.foreignKeys?.removeds ||
        []) {
        this.dropForeignKey(table, name);
      }

      for (const { source, target, changes } of tableChanges.changes
        ?.foreignKeys?.changes || []) {
        if (
          changes?.columns ||
          changes?.referenceTable ||
          changes?.referenceSchema ||
          changes?.referenceColumns
        ) {
          this.dropForeignKey(table, target!.name);
          this.addForeignKey(table, source!);
          if (source?.comment) {
            this.commentConstraint(table, source!.name, source!.comment);
          }
        } else if (changes?.comment) {
          this.commentConstraint(table, target!.name, source?.comment);
        }
      }
    }

    // ADD CONSTRAINT AND CHANGE COMMENT
    if (tableChanges.changes?.constraints) {
      for (const constraint of tableChanges.changes.constraints.addeds || []) {
        this.addConstraint(table, constraint);

        if (constraint.comment) {
          this.commentConstraint(table, constraint.name, constraint.comment);
        }
      }

      for (const { source, target, changes } of tableChanges.changes.constraints
        .changes || []) {
        if (
          changes?.kind !== target?.kind ||
          (source?.kind === 'CHECK' &&
            (
              tableChanges.changes
                .constraints as ObjectDifference<CheckConstraintSchema>
            ).changes?.sql)
          // || (source?.kind === 'UNIQUE' &&
          //   (
          //     tableChanges.changes
          //       .constraints as ObjectDifference<UniqueConstraintSchema>
          //   ).changes?.columns)
        ) {
          this.addConstraint(table, source!);
          if (source?.comment) {
            this.commentConstraint(table, target!.name, source.comment);
          }
        } else if (changes?.comment) {
          this.commentConstraint(table, target!.name, source?.comment);
        }
      }
    }

    // ADD INDEXES AND CHANGE COMMENT
    if (tableChanges.changes?.indexes) {
      for (const index of tableChanges.changes.indexes.addeds || []) {
        this.createIndex(table, index);
        if (index.comment) {
          this.commentIndex(table, index.name, index.comment);
        }
      }

      for (const { source, target, changes } of tableChanges.changes.indexes
        .changes || []) {
        if (changes?.columns || changes?.isClustered || changes?.isUnique) {
          this.createIndex(table, source!);
          if (source?.comment) {
            this.commentIndex(table, source.name, source.comment);
          }
        } else if (changes?.comment) {
          this.commentIndex(table, source!.name, source?.comment);
        }
      }
    }

    if (tableChanges.changes?.comment) {
      this.commentTable(table, tableChanges.source?.comment);
    }

    // WARN: 修改表名无法追踪。
    // // 如果修改了表名
    // if (tableChanges.changes.name) {
    //   codes.push(
    //     `migrate.renameTable(${JSON.stringify(
    //       tableChanges.changes.name.source
    //     )}, ${JSON.stringify(tableChanges.changes.name.target)})`
    //   );
    // }
  }

  abstract dropTable(table: TableSchema): void;

  dropTableAndMembers(table: TableSchema): void {
    table.foreignKeys.forEach(fk => this.dropForeignKey(table, fk.name));
    this.dropTable(table);
  }

  abstract annotation(note: string): void;

  abstract createSequence(sequence: SequenceSchema): void;

  abstract dropSequence(name: XObjectName): void;

  abstract addColumn(table: XObjectName, schema: ColumnSchema): void;

  abstract dropColumn(table: XObjectName, name: string): void;

  abstract alterColumn(table: XObjectName, column: ColumnSchema): void;

  abstract addPrimaryKey(table: XObjectName, pk: PrimaryKeySchema): void;
  abstract dropPrimaryKey(table: XObjectName, name: string): void;

  abstract addConstraint(
    table: XObjectName,
    constraint: ConstraintSchema
  ): void;

  abstract dropConstraint(
    table: XObjectName,
    constraint: ConstraintSchema
  ): void;
}

// export function generateScripts(
//   source: DatabaseSchema | undefined,
//   target: DatabaseSchema | undefined,
//   scripter: MigrateScripter
//   // resolverType?: (type: string) => DbType
// ): void {}

// export function generateUpdateStatements(
//   builder: MigrateBuilder,
//   source: DatabaseSchema | undefined,
//   target: DatabaseSchema | undefined
//   // resolverType?: (type: string) => DbType
// ): Statement[] {
//   const scripter = new StatementMigrateScripter(builder, source, target);
//   generateScripts(source, target, scripter);
//   return scripter.getScripts();
// }

// export function generateUpdatePrograme(
//   source: DatabaseSchema | undefined,
//   target: DatabaseSchema | undefined,
//   sqlifier: SqlUtil,
//   resolverType?: (type: string) => DbType
// ): string[] {
//   const scripter = new ProgramMigrateScripter(sqlifier, resolverType);
//   generateScripts(source, target, scripter);
//   return scripter.getScripts();
// }
